# JavaScript 模块化
# 一、CommonJS 规范
- 主要应用于服务器端(如 Node.js)
- 同步加载模块
- 每个文件即模块
# 导出模块
一个模块可以通过 module.exports 或 exports 来导出其内部的变量、函数或对象,使其他模块能够访问这些导出内容。
// hello.js
function Hello() {
console.log('Hello');
}
// 方式一:exports 简写
exports.Hello = Hello;
// 方式二:完整导出
module.exports = Hello;
# 导入模块
在其他模块中,可以使用 require 函数来导入另一个模块的导出内容。导入的模块会被加载并执行,然后返回其导出内容。
// 解构导入
const { Hello } = require('./hello');
// 基本导入
const Hello = require('./hello');
# 浏览器支持
模块名的解析是基于 Node.js 的模块系统,在浏览器环境下是不支持的。
在前端开发中,通常需要使用工具(如 Browserify、Webpack 等)来转换和处理模块名的导入。
以 Browserify 为例:
browserify ./index.js -o ./bundle.js
# 二、AMD 规范
所有依赖必须在模块定义时提前声明,并通过异步回调使用。
- 依赖前置声明:模块定义时需明确列出所有依赖,加载器会提前并行加载。
- 模块提前执行:依赖加载完成后,模块会立即执行。
// AMD(RequireJS)
define(['./a', './b'], function(a, b) {
a.doSomething(); // 所有依赖已提前加载并执行
b.doSomethingElse();
});
# 三、CMD 规范
CMD 是通用模块加载,要解决的问题与 AMD 一样,只不过是对依赖模块的执行时机不同,推崇就近依赖。
- 就近依赖:依赖在模块内部按需声明,通常在使用到的地方通过
require同步引入。 - 延迟执行:模块整体加载后不会立即执行,而是在遇到
require语句时才会执行依赖模块。
// CMD(Sea.js)
define(function(require, exports, module) {
var a = require('./a'); // 依赖就近声明
a.doSomething();
if (condition) {
var b = require('./b'); // 按需加载依赖
b.doSomethingElse();
}
});
AMD 和 CMD 的核心区别:
| 特性 | CMD(就近依赖) | AMD(依赖前置) |
|---|---|---|
| 依赖声明 | 代码内部就近声明,可条件化 | 模块顶部提前声明,静态化 |
| 加载时机 | 执行到 require 时加载 | 定义时并行加载所有依赖 |
| 执行时机 | 延迟执行(按需执行) | 提前执行(加载完立即执行) |
| 适用场景 | 适合需要按需加载的复杂逻辑 | 适合依赖明确且需快速执行的场景 |
| 代码灵活性 | 高(依赖可动态引入) | 低(依赖必须静态声明) |
# 模块定义
Sea.js 是 CMD 规范的一个实现。
定义模块使用全局函数 define(),接收一个 factory 参数,可以是一个函数,也可以是一个对象或字符串。
factory 是函数时有三个参数:
- require:用来获取其他模块提供的接口。
- exports:用来向外提供本模块接口。
- module:存储了与当前模块相关联的属性和方法。
// 函数式定义
define(function (require, exports, module) {
// 同步引入
const mod = require('./module');
// 异步引入
require.async('./module', mod => {
// 回调处理
});
});
// 简单值定义
define({ foo: 'bar' });
define(function (require, exports, module) {
function Hello() {
console.log('Hello');
};
// 导出
module.exports = { Hello };
});
# 入口文件
在 index.html 中引入 sea.js,并调用 main.js 入口主模块。
<script src="https://cdn.bootcdn.net/ajax/libs/seajs/3.0.3/sea.js"></script>
<script>
seajs.use('./main.js');
</script>
# 四、ES Modules
ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出变量。
- 静态编译时加载
- 浏览器原生支持
- 支持 Tree Shaking
// ES6 Module(静态导入)
import a from './a';
a.doSomething();
// 动态按需加载(类似CMD思想)
if (condition) {
const b = await import('./b');
b.doSomethingElse();
}
# 导出方式
命名导出
使用命名导出时,可以将模块中的多个变量、函数或类分别导出,以便其他模块可以通过相应的名称进行导入。
// 行内导出 export const add = (a, b) => a + b; // 批量导出 export { add, subtract };默认导出
默认导出用于导出模块的主要内容,一个模块只能有一个默认导出。在导入时,可以选择为默认导出指定任何名称。
const myFunction = (a, b) => a + b; export default myFunction;全量导出
export * from 'fs'用于将另一个模块的所有导出内容重新在当前模块导出。在这个特定的例子中,它会将 Node.js 的内置模块
fs的所有导出内容重新在当前模块导出。// myModule.js export * from 'fs';然后,可以在其他模块中来访问这些导出的内容。
// 在其他模块中导入 import { readFile, writeFile } from './myModule.js';
# 导入方式
命名导入
import { add, subtract } from './math.js';默认导入
import Calculator from './math.js';混合导入
import React, { Component } from 'react';全量导入
import * as fs from 'fs' import { default as fs } from 'fs'
# 浏览器支持
<script type="module">
import { add } from './math.js';
</script>